package com.xvxingan.base.shiro;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.xvxingan.base.dao.BaseManager;
import com.xvxingan.utils.EncryptUtil;

/**
 * HTTP摘要认证服务器端拦截器。
	参考网址：
	http://www.faqs.org/rfcs/rfc2617.html
	http://www.faqs.org/rfcs/rfc1321.html
	http://www.cnblogs.com/my_life/articles/2285649.html
	http://blog.sina.com.cn/s/blog_53b15ed5010006t9.html
	http://blog.chinaunix.net/uid-26212859-id-3485297.html
	http://blog.csdn.net/jackxinxu2100/article/details/5610448#reply
	http://www.vsharing.com/k/KM/2003-1/458471.html
	http://support.microsoft.com/kb/811318/zh-cn
	
	认证的过程总体上是：客户端请求资源->服务器返回认证标示->客户端发送认证信息->服务器查验认证，如果成功则继续资源传送，否则直接断开连接。
	服务器返回认证标示的必要内容如下：
	
	WWW-Authenticate: Digest realm="xx",qop="auth,auth-int",nonce="xx",opaque="xx"
	
	这个部分需要和http的协议头部分一起，即放在第一个空行的前面，其中的双引号也一同传输。
	各域的值中，不允许出现冒号“:”，因为冒号在md5加密时是个连接符。
	同时返回401的Unauthorized错误。
	默认是md5加密，而且md5应该是用的最广的，所以对于algorithm域，无需添加。
	
	WWW-Authenticate是httpd的一个标头
	
	realm的值是一个简单的字符串，而rfc2617上写的是一个email类型的字符串，我看这个没有必要意义，所以我就也简单地写了个email形式的字符串
	
	qop是认证的(校验)方式
	
	nonce的值也是一个字符串，如果不严格，可以随机生成一个就行，注意它是个GUID，即唯一的、不重复的。
	
	opaque是个字符串，它只是透传而已，即客户端还会原样返回过来。实际上，上面的那些域，客户端都还是会原样返回的，但返回时除了以上的那些域之外，还会增加新的内容进来。
	客户端在收到401错误之后，根据上面的那些信息，需要用户输入用户名和密码，然后返回给服务器如下内容：
	
	Authorization: Digest username="xx",realm="xx",qop="auth",nonce="xx",uri="path",cnonce="xx",nc=00000001,response="xx",opaque="xx"
	
	除了qop的值不一样之外，其它存在的那些域的值都是一样的，新增加的部分：
	
	username是要认证的用户名
	
	uri是本次请求的资源位置，比如“/public/userinfo.htm”
	
	cnonce是客户端产生的一个GUID，一般是32字节，而且是16个字节字符串的16进制形式表示，所以其中的内容是0~9和a~f之间的那些字符。其实nonce也是一样，但我没有特意这样做，
	比如我直接生成一个数字，然后以16进制输出，而没有特殊追求16个字节。
	
	nc是认证的次数，因为如果认证失败，则仍然可以重新发送认证信息继续认证，第一次是1，第二次是2，第三次是3，...。但我这里没有这样弄，只允许一次，
	如果认证不过就关闭了连接，所以我只处理了1的情况，即00000001，这个值总是固定的8个字节，而且不加引号，和其它域的格式不一样，导致我在分析这个域的时候单独写了个代码，
	晕，不知道为什么会定义的不一样。
	
	response的值就很重要了，是根据以上信息，再加上密码通过一定的顺序计算出的一个md5码，固定为16字节的16进制表示形式。服务器在收到所有这些信息后，
	也通过相同的方式计算出这个值，而密码则是保存在服务器端，即服务器要通过用户名去找到对应的密码，然后和计算出md5值，再和客户端传过来的response值对比，如果一样，
	则认证通过，否则通不过。
	关键的是md5值的计算，用各域的值进行计算（不带两端的双引号）。
	
	总的计算公式是：md5=HA1:HD:HA2
	
	注意，这个公式被我简化了，只是为了便于描述而和其它文档上的形式不一样。表示的是对HA1:HD:HA2进行md5计算，下同。
	
	md5表示最终计算出来的值，即response的值
	
	注意之间的冒号“:”也是计算的字符串中的内容，是个连接符
	如果algorithm的值是“md5-sess”，则
	HA1=username:realm:password:nonce:cnonce (1)
	否则
	HA1=username:realm:password  (2)
	我这里用的是(2)
	如果qop有值(if (*pszQop))，则
	HD=nonce:noncecount:cnonce:qop (3)
	否则
	HD=nonce (4)
	由于我这里qop有值，所以用了(3)
	HA2=method:uri
	如果qop的值是“auth-int”，则HA2的值计算不一样，我看的不是很懂，而且我没有用，所以这个我没有管它。
	method是指“GET”/"POST"，即http头中指定的获取资源的方式
*/

@Component
public class HTTPDigestAuthenticateInterceptor extends HandlerInterceptorAdapter {
	
	private static final Logger logger = Logger.getLogger(HTTPDigestAuthenticateInterceptor.class);
	private String realm;
	private static final String AUTHORIZATION_HEADER = "Authorization";
	private static final String AUTHENTICATE_HEADER = "WWW-Authenticate";
	private BaseManager baseManager;
	
	public BaseManager getBaseManger() {
		return baseManager;
	}
	
	public void setBaseManager(BaseManager baseManager) {
		this.baseManager = baseManager;
	}

	public HTTPDigestAuthenticateInterceptor() {
		this.realm = "localhost:8080";
	}

	/**
	 * 设置认证域。 
	 * @param realm 认证域
	 */
	public void setRealm(String realm) {
		logger.debug(String.format("注入认证域：%s", realm));
		this.realm = realm;
	}

	/*
	 * 重载处理。
	 */
	@Override
	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception {

		logger.debug("开始摘要认证处理...");
		// 验证摘要。
		if (!this.authentication(request)) {
			// 认证失败，发送401错误。
			this.sendAuthenticate(response);
			return false;
		}
		logger.debug("摘要认证处理完毕!");
		return super.preHandle(request, response, handler);
	}

	/**
	 * 发送401错误认证信息。 
	 * realm 认证的域realm 
	 * qop "auth" 保护质量参数 
	 * nonce 服务器端的随机数一起带回 透传参数
	 * opaque 透传参数 暂未用
	 * 
	 * @param response
	 */
	private void sendAuthenticate(HttpServletResponse response) {

		logger.debug("Authentication required: sending 401 Authentication challenge response.");
		String randomCode = createRandomCode(), qop = "auth";
		StringBuilder authcHeadBuilder = new StringBuilder();
		authcHeadBuilder
				.append(HttpServletRequest.DIGEST_AUTH)
				.append(" ")
				.append("realm")
				.append("=")
				.append("\"")
				.append(this.realm)
				.append("\",")
				.append("qop")
				.append("=")
				.append("\"")
				.append(qop)
				.append("\",")
				.append("nonce")
				.append("=")
				.append("\"")
				.append(randomCode)
				.append("\",")
				.append("opaque")
				.append("=")
				.append("\"")
				.append(EncryptUtil
						.MD5Encode(this.realm + ":" + qop + ":" + randomCode))
				.append("\"");
		String authc_head = authcHeadBuilder.toString();
		logger.debug(String.format("http-head:%s", authc_head));
		response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
 
		response.setHeader(AUTHENTICATE_HEADER, authc_head);
		response.setHeader("statuscode", "-"+HttpServletResponse.SC_UNAUTHORIZED);
	}

	// 生成随机码。
	private static String createRandomCode() {
		return UUID.randomUUID().toString().replaceAll("-", "").toLowerCase();
	}
 
	/**
	 * 验证摘要
	 * username 用户登录名 
	 * realm 域名
	 * nonce 服务器随机数 
	 * uri 本次请求的uri 
	 * qop "auth" 保护质量参数
	 * nc 0000001 
	 * cnonce 客户端随机数 
	 * response 用于验证比较response= MD5.MD5Encode(ha1 + ":" + nonce_value + ":"+ nc_value + ":" + cnonce_value + ":" + qop_value + ":" + ha2) 
	 * opaque  用于验证比较opaque = MD5.MD5Encode(this.realm + ":" + qop_value + ":"+ nonce_value);
	 * 
	 * @param method
	 *            http请求方式
	 * @param authz
	 *            http头部字段Authorization
	 * @return
	 */
	private synchronized boolean authentication(HttpServletRequest request) {
		String method = request.getMethod();
		String authz = request.getHeader(AUTHORIZATION_HEADER);
		logger.debug(String.format("验证摘要：%s", authz));
		//非空校验
		String username_value = this.getParameter(authz, "username");
		String realm_value = this.getParameter(authz, "realm");
		String nonce_value = this.getParameter(authz, "nonce");
		String uri_value = this.getParameter(authz, "uri");
		String qop_value = this.getParameter(authz, "qop");
		String nc_value = this.getParameter(authz, "nc");
		String cnonce_value = this.getParameter(authz, "cnonce");
		String response_value = this.getParameter(authz, "response");
		String opaque_value = this.getParameter(authz, "opaque");
		if(anyEmtyp(new String[]{authz,username_value,realm_value,nonce_value,uri_value,qop_value,nc_value,cnonce_value,response_value,opaque_value})){
			return false;
		}
		// 验证opaque
		String opaque = EncryptUtil.MD5Encode(this.realm + ":" + qop_value + ":"+ nonce_value);
		if (!opaque.equalsIgnoreCase(opaque_value)) {
			logger.debug("验证opaque失败！");
			return false;
		}
		Map<String, Object> parameters = new HashMap<String,Object>();
 		parameters.put("email", username_value);
/*		User u = (User) this.baseManager.get("com.up4years.cn.entity.User.getUserInfoByEmail", parameters);
		if(u==null){
			return false;
		}
		String passwd = u.getPasswd();
		//用户名:域名:密码
		String ha1 = EncryptUtil.MD5Encode(username_value + ":" + realm_value + ":" + passwd);
		String ha2 = EncryptUtil.MD5Encode(method + ":" + uri_value);
		String response = EncryptUtil.MD5Encode(ha1 + ":" + nonce_value + ":"+ nc_value + ":" + cnonce_value + ":" + qop_value + ":" + ha2);
		return response.equalsIgnoreCase(response_value);*/
 		return false;
	}
	/**
	 * 判断是否有一个元素为空 如果有 返回true
	 * @param arr
	 * @return
	 */
	private boolean anyEmtyp(String[] arr){
		for(String s :arr){
			if(StringUtils.isEmpty(s))return false;
		}
		return false;
	}
	// 获取参数。
	private String getParameter(String authz, String name) {
		if (StringUtils.isEmpty(authz) || StringUtils.isEmpty(name))
			return null;
		String regex = name + "=((.+?,)|((.+?)$))";
		Matcher m = Pattern.compile(regex).matcher(authz);
		if (m.find()) {
			String p = m.group(1);
			if (!StringUtils.isEmpty(p)) {
				if (p.endsWith(",")) {
					p = p.substring(0, p.length() - 1);
				}
				if (p.startsWith("\"")) {
					p = p.substring(1);
				}
				if (p.endsWith("\"")) {
					p = p.substring(0, p.length() - 1);
				}
				return p;
			}
		}
		return null;
	}

}